// kcp client logic abstracted into a class.
// for use in Mirror, DOTSNET, testing, etc.
using System;

namespace kcp2k
{
    public class KcpClient
    {
        // events
        public Action OnConnected;
        public Action<ArraySegment<byte>, KcpChannel> OnData;
        public Action OnDisconnected;
        // error callback instead of logging.
        // allows libraries to show popups etc.
        // (string instead of Exception for ease of use and to avoid user panic)
        public Action<ErrorCode, string> OnError;

        // state
        public KcpClientConnection connection;
        public bool connected;

        public KcpClient(Action OnConnected,
                         Action<ArraySegment<byte>,
                         KcpChannel> OnData,
                         Action OnDisconnected,
                         Action<ErrorCode, string> OnError)
        {
            this.OnConnected = OnConnected;
            this.OnData = OnData;
            this.OnDisconnected = OnDisconnected;
            this.OnError = OnError;
        }

        // CreateConnection can be overwritten for where-allocation:
        // https://github.com/vis2k/where-allocation
        protected virtual KcpClientConnection CreateConnection() =>
            new KcpClientConnection();

        public void Connect(string address,
                            ushort port,
                            bool noDelay,
                            uint interval,
                            int fastResend = 0,
                            bool congestionWindow = true,
                            uint sendWindowSize = Kcp.WND_SND,
                            uint receiveWindowSize = Kcp.WND_RCV,
                            int timeout = KcpConnection.DEFAULT_TIMEOUT,
                            uint maxRetransmits = Kcp.DEADLINK,
                            bool maximizeSendReceiveBuffersToOSLimit = false)
        {
            if (connected)
            {
                Log.Warning("KCP: client already connected!");
                return;
            }

            // create connection
            connection = CreateConnection();

            // setup events
            connection.OnAuthenticated = () =>
            {
                Log.Info($"KCP: OnClientConnected");
                connected = true;
                OnConnected();
            };
            connection.OnData = (message, channel) =>
            {
                //Log.Debug($"KCP: OnClientData({BitConverter.ToString(message.Array, message.Offset, message.Count)})");
                OnData(message, channel);
            };
            connection.OnDisconnected = () =>
            {
                Log.Info($"KCP: OnClientDisconnected");
                connected = false;
                connection = null;
                OnDisconnected();
            };
            connection.OnError = (error, reason) =>
            {
                OnError(error, reason);
            };

            // connect
            connection.Connect(address,
                               port,
                               noDelay,
                               interval,
                               fastResend,
                               congestionWindow,
                               sendWindowSize,
                               receiveWindowSize,
                               timeout,
                               maxRetransmits,
                               maximizeSendReceiveBuffersToOSLimit);
        }

        public void Send(ArraySegment<byte> segment, KcpChannel channel)
        {
            if (connected)
            {
                connection.SendData(segment, channel);
            }
            else Log.Warning("KCP: can't send because client not connected!");
        }

        public void Disconnect()
        {
            // only if connected
            // otherwise we end up in a deadlock because of an open Mirror bug:
            // https://github.com/vis2k/Mirror/issues/2353
            if (connected)
            {
                // call Disconnect and let the connection handle it.
                // DO NOT set it to null yet. it needs to be updated a few more
                // times first. let the connection handle it!
                connection?.Disconnect();
            }
        }

        // process incoming messages. should be called before updating the world.
        public void TickIncoming()
        {
            // recv on socket first, then process incoming
            // (even if we didn't receive anything. need to tick ping etc.)
            // (connection is null if not active)
            connection?.RawReceive();
            connection?.TickIncoming();
        }

        // process outgoing messages. should be called after updating the world.
        public void TickOutgoing()
        {
            // process outgoing
            // (connection is null if not active)
            connection?.TickOutgoing();
        }

        // process incoming and outgoing for convenience
        // => ideally call ProcessIncoming() before updating the world and
        //    ProcessOutgoing() after updating the world for minimum latency
        public void Tick()
        {
            TickIncoming();
            TickOutgoing();
        }
    }
}
